/*
 * Copyright (C) 2011 Michael Widmer <widmm8@bfh.ch>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package ch.bfh.aeneas.core.location;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;

/**
 * PositionManager is responsible for getting position fixes based on gps and
 * network orientation mechanisms.
 * Beware: PositionManager only looks for updates if there is at least one 
 * listener registered.
 * @author Michael Widmer <widmm8@bfh.ch>
 */
public class PositionManager extends Service {
    
    /**
     * Components interested in position updates should be implementing this 
     * interface and register themself on this service by calling addOnLocationChangedListener.
     * @see #addOnLocationChangedListener(ch.bfh.aeneas.core.location.PositionManager.OnPositionChangedListener)
     * @see #removeOnLocationChangedListener(ch.bfh.aeneas.core.location.PositionManager.OnPositionChangedListener) 
     */
    public interface OnPositionChangedListener {
        public void onPositionFix(Location l);
        public void onPositionChanged(Location l);
    }
    
    /*
    private PhoneStateListener GSMListener = new PhoneStateListener() {        
        public void onSignalStrenthsChanged(SignalStrength signalStrength) {
            mGSMStrength = signalStrength.getGsmSignalStrength();
        }        
    };
    
    private GpsStatus.Listener GPSListener = new GpsStatus.Listener() {
        public void onGpsStatusChanged(int event) {
            GpsStatus gs = lmgr.getGpsStatus(null);
            mGPSStrength = gs.getMaxSatellites();
        }
    };*/
    
    private LocationListener mLocListener = new LocationListener() {
        public void onLocationChanged(Location loc) {            
            handleNewLocation(loc);
        }        
        public void onStatusChanged(String provider, int status, Bundle extras) {}
        public void onProviderEnabled(String provider) {}
        public void onProviderDisabled(String provider) {}
    };

    public class PositionManagerBinder extends Binder {
        public PositionManager getService() {
            return PositionManager.this;
        }
    }
    private final PositionManagerBinder mBinder = new PositionManagerBinder();
    
    private final static String LOG_TAG = "PositionManager";
    
    protected static final int UPDATE_RANGE = 1000 * 15;
    protected LocationManager lmgr;
    protected boolean mListening;
    protected boolean mIsUptodate;
    protected int mGSMStrength;
    protected int mGPSStrength;
    protected Location mLastLocation;
    private List<OnPositionChangedListener> mChangeListeners;

    @Override
    /**
     * {@inheritDoc}
     */
    public void onCreate() {
        super.onCreate();
        
        mListening = false;
        mIsUptodate = false;
        mChangeListeners = new ArrayList<OnPositionChangedListener>();
        mGSMStrength = -1;
        mGPSStrength = -1;
    }
    
    @Override
    /**
     * {@inheritDoc}
     */
    public void onDestroy() {
        
        super.onDestroy();
    }
    
    @Override
    /**
     * {@inheritDoc}
     */
    public void onStart(Intent intent, int startId) {
        onStartService(intent);
    }

    @Override
    /**
     * {@inheritDoc}
     */
    public int onStartCommand(Intent intent, int flags, int startId) {
        onStartService(intent);
        return START_STICKY;
    }
    
    private void onStartService(Intent intent) {
        
    }
    
    @Override
    /**
     * {@inheritDoc}
     */
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    
    /**
     * Add a new listener to get updates on position updates.
     * @param listener 
     */
    public void addOnLocationChangedListener(OnPositionChangedListener listener) {
        if(!mChangeListeners.contains(listener)) {
            mChangeListeners.add(listener);
            
            if(mChangeListeners.size() > 0 && !mListening) {
                addListeners();
            } else {
                if(mIsUptodate) {
                    listener.onPositionFix(mLastLocation);
                }
            }
        }
    }
    
    /**
     * Removes the listener if previously registered.
     * @param listener 
     */
    public void removeOnLocationChangedListener(OnPositionChangedListener listener) {
        if(mChangeListeners.contains(listener)) {
            mChangeListeners.remove(listener);
            
            if(mChangeListeners.size() < 1 && mListening) {
                removeListeners();
            }
        }
    }
    
    protected void addListeners() {
        Context c = getApplicationContext();
        lmgr = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);
        lmgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mLocListener);
        lmgr.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, mLocListener);
        
        mLastLocation = lmgr.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        mIsUptodate = false;
        mListening = true;
    }
    
    protected void removeListeners() {
        Context c = getApplicationContext();
        lmgr = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);
        lmgr.removeUpdates(mLocListener);
        mListening = false;
        mIsUptodate = false;
    }

    private void handleNewLocation(Location loc) {
        Log.i(LOG_TAG, "Received new location with accuracy: "+loc.getAccuracy()+" from provider "+loc.getProvider());
        if(isBetterLocation(loc)) {
            if(mChangeListeners.size() > 0) {
                for(int i = 0; i < mChangeListeners.size(); i++) {
                    if(!mIsUptodate) {
                        mChangeListeners.get(i).onPositionFix(loc);
                    } else {
                        mChangeListeners.get(i).onPositionChanged(loc);
                    }
                }
            }
            if(!mIsUptodate) {
                mIsUptodate = true;
            }
        }
    }
    
    /** 
     * Determines whether one Location reading is better than the current Location fix
     * @param location  The new Location that you want to evaluate
     */
    protected boolean isBetterLocation(Location location) {
        
        // If no location was available new one is better anyway
        if(mLastLocation == null) {
            return true;
        }
        // Check whether the new location fix is newer or older
        long timeDelta = location.getTime() - mLastLocation.getTime();
        boolean isSignificantlyNewer = timeDelta > UPDATE_RANGE;
        boolean isSignificantlyOlder = timeDelta < -UPDATE_RANGE;
        boolean isNewer = timeDelta > 0;

        // If it's been more than two minutes since the current location, use the new location
        // because the user has likely moved
        if (isSignificantlyNewer) {
            return true;
        // If the new location is more than two minutes older, it must be worse
        } else if (isSignificantlyOlder) {
            return false;
        }

        // Check whether the new location fix is more or less accurate
        int accuracyDelta = (int) (location.getAccuracy() - mLastLocation.getAccuracy());
        boolean isLessAccurate = accuracyDelta > 0;
        boolean isMoreAccurate = accuracyDelta < 0;
        boolean isSignificantlyLessAccurate = accuracyDelta > 200;

        // Check if the old and new location are from the same provider
        boolean isFromSameProvider = isSameProvider(location.getProvider(),
                mLastLocation.getProvider());

        // Determine location quality using a combination of timeliness and accuracy
        if (isMoreAccurate) {
            return true;
        } else if (isNewer && !isLessAccurate) {
            return true;
        } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
            return true;
        }
        return false;
    }

    /** Checks whether two providers are the same */
    private boolean isSameProvider(String provider1, String provider2) {
        if (provider1 == null) {
            return provider2 == null;
        }
        return provider1.equals(provider2);
    }
}
